#!/usr/bin/env ruby
#
# Test runner for arbitrary executables.
#
# (c) 2008 Martin Suesskraut
#       suesskraut@se.inf.tu-dresden.de
#
# The file with the expected output is optional. If it is found, then
# the output of the cmd is compared to the expected output. The test
# fails either of the cmd does not succeed (exit code 0) or (if given)
# the expected output differs from the output of the cmd.
#
# The output of the command is written to
# logs/<test case>.log
# The diff between actual output and expected output is written to
# logs/<test case>.diff

require 'fileutils'
require 'yaml'
require 'pp'

EXPECTED_OUT_FMT = "%s.txt"
LOG_FMT = "logs/%s.log"
DIFF_LOG_FMT = "logs/%s.diff"
TEST_NAME_LENGTH = 60
TEST_CASES = 'testcases.yaml'
SUITES_FILE = 'suites.lst'

includes = ARGV.dup.map { |reg| /^#{reg}$/ }
includes = [/.*/] if includes.empty?

suite_names = File.readlines('suites.lst').map { |line| line.strip }.select do |line|
    line.length > 0  && !['#', ';'].include?(line[0, 1]) && includes.any? { |inc| line =~ inc }
end

testcases = YAML::load_file('testcases.yaml')
class << testcases
    def deep_clone(obj, subst)
        case obj
            when Array
                return obj.map { |e| deep_clone(e, subst) }
            when Hash
                h = {}
                obj.each { |k, v| h[deep_clone(k, subst)] = deep_clone(v, subst) }
                return h
            when String
                return obj.gsub(/\$\(([^)]+)\)/) { v = subst[$1.to_sym]; v ? v : '$(%s)' % $1 }
            when Symbol
                return obj
            else fail "unknown obj-class '#{obj.class.inspect}' to deep_clone in: '#{obj.inspect}'"
        end
    end

    def dclone(subst = {})
        deep_clone(self, subst)
    end

    private :deep_clone
end

suites = {}
suite_names.each do |suite|
    suites[suite] = testcases.dclone(:SUITE => suite)
end

suites.length > 0 or fail "no tests given"

FileUtils.mkdir_p "logs"

def print_name(name)
    print_name = name.length > TEST_NAME_LENGTH - 3 ? "%s..." % [name[0, TEST_NAME_LENGTH - 5]] : name
end

def parse_test_arg(name)
    log = LOG_FMT % name
    diff_log = DIFF_LOG_FMT % name
    return log, diff_log
end

def run_cmd(cmd, log)
    system("bash -O execfail -c 'command %s' >%s 2>&1" % [cmd, log])
end

puts "running %d suite(s)" % suites.length
failed = Hash.new { |h, k| h[k] = [] }
suites.keys.sort.each_with_index do |suite, i|
    cases = suites[suite]
    print "\t%3d/%3d   %#{TEST_NAME_LENGTH}s " % [i + 1, suites.length, print_name(suite)]
    $stdout.flush
    cases.each_with_index do |test, j|
	log, diff_log = parse_test_arg(test[:name])
	cmd = test[:cmd]
	expected_output = test[:exp_out]
	res = run_cmd(cmd, log)
	if res && expected_output && File.exist?(expected_output)
	    res = system("diff %s %s >%s 2>&1" % [expected_output, log, diff_log])
	else
	    FileUtils.rm_f diff_log
	end
	print(res ? "." : "*")
	$stdout.flush
	failed[suite] << test[:name] unless res
    end
    if failed[suite].length > 0
	puts " %d/%d FAILED" % [failed[suite].length, cases.length]
    else
	puts "\b" * cases.length + "\e[KPASSED"
    end
end

failed.reject! { |suite, cases| cases.empty? }

puts "%d of %d suite(s) failed" % [failed.length, suites.length]
failed.each do |suite, cases|
    next unless cases.length > 0
    puts "%d/%d in %s" % [cases.length, suites[suite].length, suite]
    cases.each do |caze|
	puts "\t%s" % [caze]
    end
end
exit failed.length > 0 ? 1 : 0
